-- 搜索并打开modem外设，初始化rednet网络
peripheral.find("modem", rednet.open)
local properties, system
local protocol, request_protocol = "CBCNetWork", "CBCcenter"
local lastSpawnerPos = nil
local selfId = os.getComputerID()
----------------init-----------------
system = {
    fileName = "spawner_dat", 
    file = nil,      
}
system.init = function()
    system.file = io.open(system.fileName, "r")
    if system.file then
        local tmpProp = textutils.unserialise(system.file:read("a"))
        properties = system.reset()
        for k, v in pairs(properties) do
            if tmpProp[k] then
                properties[k] = tmpProp[k]
            end
        end
        system.file:close()
    else
        properties = system.reset()
        system.updatePersistentData()
    end
end
system.reset = function()
    return {
        spawnerName = "SP",                -- 系统名称
        controlCenterId = "-1",            -- 控制中心ID
        power_on = "back",                 -- 开机信号端口
        spawnerHeight = 5,                 -- 刷怪笼生成高度(目标上方)
        password = "123456",               -- 密码
        mobType = "cbc",                   -- mod类型
        Entity = "he",                     -- 生成实体
        spawnCount = 4,                    -- 每次生成数量
        spawnRange = 4,                    -- 生成范围
        Gravity = 0.05,                    -- 重力加速度(方块/帧^2)
        despawnTime = 1,                   -- 刷怪笼存在时间(秒)
        Distance = 10,                     -- 引爆距离(方块)
        Coolingtime = 5,                   -- 冷却时间(s)
        INPUT = "back",
    }
end

system.updatePersistentData = function()
    system.write(system.fileName, properties)
end

system.write = function(file, obj)
    system.file = io.open(file, "w")
    system.file:write(textutils.serialise(obj))
    system.file:close()
end

system.init()
commands.execAsync(string.format(
    "/gamerule commandBlockOutput false"
   ))

local modtype = {
    mc = "minecraft:",
    cbc = "createbigcannons:",
    cbcmw = "cbcmodernwarfare:",
    warium = "shupapium:"
}
local mc = {
    fireball = "fireball",
    fire = "small_fireball",
    arrow = "spectral_arrow",
    bolt = "lightning_bolt",
    snow = "snowball",
    dragon = "dragon_fireball",
    trident = "trident"
}
local cbc = {
    he = "he_shell",
    smoke = "smoke_shell",
    mortar = "mortar_stone",
    drop = "drop_mortar_shell",
    shrapnel = "shrapnel_shell",
}
local cbcmw = {
    heap = "heap_shell",
    hefrag = "hefrag_shell",
    apds = "apds_mediumshell"
}
local warium = {
    ord = "ordinance_shell",
    Rord = "redordinance_shell",
    Hord = "heavyordinance_shell",
    Sord = "superheavyordinance_shell",
    clu = "cluster_shell",
    kin = "kinetic_shell",
    tox = "toxicgas_shell",
    fis = "fission_shell",
    fus = "fusion_shell"
}
local fuze = {
    norm = "{id:\"createbigcannons:timed_fuze\",tag:{FuzeTimer:200},Count:1b}",
    proximity = "{id:\"createbigcannons:proximity_fuze\",tag:{DetonationDistance:2},Count:1b}",
    m = "{id:\"createbigcannons:delayed_impact_fuze\",tag:{FuzeTimer:%d},Count:1b}",
    impact = "{id:\"createbigcannons:impact_fuze\",Count:1b}"
}

local controlCenter = { 
    tgPos = { x = 0, y = 0, z = 0 }, 
    velocity = { x = 0, y = 0, z = 0 }, 
    mode = 2, 
    fire = false 
}

local ct = 20

local listener = function()
    local controlCenterId = tonumber(properties.controlCenterId) or 0
    while true do
        local id, msg = rednet.receive(protocol, 2)
        if id == controlCenterId then
            controlCenter = msg
            ct = 20
        end
    end
end

local predictTargetPosition = function(targetPos, velocity, frames)
    return {
        x = targetPos.x + velocity.x * frames,
        y = targetPos.y + velocity.y * frames,
        z = targetPos.z + velocity.z * frames
    }
end

local time = 1
local createSpawner = function(x, y, z)
    if lastSpawnerPos then
        commands.execAsync(string.format(
            "/setblock %d %d %d air",
            lastSpawnerPos.x, lastSpawnerPos.y, lastSpawnerPos.z
        ))
    end
    
    local entityId = ""
    if properties.mobType == "mc" then
        entityId = mc[properties.Entity] or "fireball"
    elseif properties.mobType == "cbc" then
        entityId = cbc[properties.Entity] or "he_shell"
    elseif properties.mobType == "cbcmw" then
        entityId = cbcmw[properties.Entity] or "heap_shell"
    elseif properties.mobType == "warium" then
        entityId = warium[properties.Entity] or "ordinance_shell"
    end

    local Cooling_time = function ()
        for i = properties.Coolingtime, 0, -1 do
            term.setCursorPos(28, 13)
            printError(string.format("%d", i))
            sleep(1)
        end
        term.setCursorPos(28, 13)
        printError("   ")
    end
    local velocity = -properties.spawnerHeight/20
    local spawnerData = string.format(
        '{Delay:5s,MaxNearbyEntities:100s,MaxSpawnDelay:20s,MinSpawnDelay:20s,RequiredPlayerRange:-1s,SpawnCount:%d,SpawnData:{entity:{id:"%s%s",Motion:[0.0,%f,0.0],Glowing:true,acceleration_power:-0.05,damage:100,Fuze:{id:"createbigcannons:proximity_fuze",tag:{DetonationDistance:%s},Count:1b}}},SpawnRange:%d}',
        properties.spawnCount,
        modtype[properties.mobType],
        entityId,
        velocity,
        properties.Distance,
        properties.spawnRange
    )
    
    local spawnX = math.floor(x)
    local spawnY = math.floor(y + properties.spawnerHeight)
    local spawnZ = math.floor(z)
    
    commands.execAsync(string.format(
        "/setblock %d %d %d minecraft:spawner%s",
        spawnX, spawnY, spawnZ,
        spawnerData
    ))
    commands.execAsync(string.format(
        "/setblock %d %d %d minecraft:sea_lantern",
        spawnX, spawnY+1, spawnZ
    ))
    commands.execAsync(string.format(
        "/playsound entity.warden.sonic_boom block @a %d %d %d 32 1 0.3",
        spawnX, spawnY, spawnZ
    ))
    for i = 2, properties.spawnerHeight-1, 1 do
        commands.execAsync(string.format(
        "/particle minecraft:sonic_boom %d %d %d %d %d %d 0 0 force",
        spawnX, spawnY-i, spawnZ,spawnX, spawnY-i, spawnZ
    ))
    end

    lastSpawnerPos = {
        x = spawnX,
        y = spawnY,
        z = spawnZ
    }

    sleep(properties.despawnTime)

    commands.execAsync(string.format(
            "/setblock %d %d %d minecraft:air",
            lastSpawnerPos.x, lastSpawnerPos.y, lastSpawnerPos.z
    ))
    commands.execAsync(string.format(
            "/setblock %d %d %d minecraft:air",
            lastSpawnerPos.x, lastSpawnerPos.y+1, lastSpawnerPos.z
    ))
    Cooling_time()
    return true
end

local sendRequest = function()
    local slug = ship and ship.getName() or nil
    while true do
        local controlCenterId = #properties.controlCenterId == 0 and 0 or tonumber(properties.controlCenterId)
        controlCenterId = controlCenterId and controlCenterId or 0
        rednet.send(controlCenterId, {
            name = properties.spawnerName,
            pw = properties.password,
            slug = slug,
            bullets_count = 0,  
            cross_point = {x=0, y=0, z=0}  
        }, request_protocol)
        sleep(0.05)
    end
end

local runControl = function()
    while true do
        if ct > 0 and controlCenter.fire or redstone.getInput(properties.INPUT) == true then
            time = (2 * properties.spawnerHeight / properties.Gravity) ^ 0.5 / 200
            local predictedPos = predictTargetPosition(
                controlCenter.tgPos,
                controlCenter.velocity,
                time
            )
            createSpawner(predictedPos.x, predictedPos.y, predictedPos.z)
            ct = 0  
        end
        sleep(0.1)
    end
end

---------------- UI 界面 -----------------
local termUtil = {
    cpX = 1,
    cpY = 1,
}

local absTextField = {
    x = 1,
    y = 1,
    len = 15,
    text = "",
    textCorlor = "0",
    backgroundColor = "8",
}

local genStr = function(s, count)
    local result = ""
    for i = 1, count, 1 do
        result = result .. s
    end
    return result
end

function absTextField:paint()
    local str = ""
    for i = 1, self.len, 1 do
        local text = tostring(self.key[self.value])
        local tmp = string.sub(text, i, i)
        if #tmp > 0 then
            str = str .. tmp
        else
            local tmp2 = ""
            for j = 0, self.len - i, 1 do
                tmp2 = tmp2 .. " "
            end
            str = str .. tmp2
            break
        end
    end

    term.setCursorPos(self.x, self.y)
    term.blit(str, genStr(self.textCorlor, #str), genStr(self.backgroundColor, #str))
end

function absTextField:inputChar(char)
    local xPos, yPos = term.getCursorPos()
    xPos = xPos + 1 - self.x
    local field = tostring(self.key[self.value])
    if #field < self.len then
        if self.type == "number" then
            if char >= '0' and char <= '9' then
                if field == "0" then
                    field = char
                else
                    field = string.sub(field, 1, xPos) .. char .. string.sub(field, xPos, #field)
                end

                self.key[self.value] = tonumber(field)
                termUtil.cpX = termUtil.cpX + 1
            end
            if char == '-' then
                self.key[self.value] = -self.key[self.value]
            end
        elseif self.type == "string" then
            local strEnd = string.sub(field, xPos, #field)
            field = string.sub(field, 1, xPos - 1) .. char .. strEnd
            self.key[self.value] = field
            termUtil.cpX = termUtil.cpX + 1
        end
    end
end

function absTextField:inputKey(key)
    local xPos, yPos = term.getCursorPos()
    local field = tostring(self.key[self.value])
    local minXp = self.x
    local maxXp = minXp + #field
    if key == 259 or key == 261 then 
        if xPos > minXp then
            termUtil.cpX = termUtil.cpX - 1
            if #field > 0 and termUtil.cpX > 1 then
                local index = termUtil.cpX - self.x
                field = string.sub(field, 1, index) .. string.sub(field, index + 2, #field)
            end
            if self.type == "number" then
                local number = tonumber(field)
                if not number then
                    self.key[self.value] = 0
                else
                    self.key[self.value] = number
                end
            elseif self.type == "string" then
                self.key[self.value] = field
            end
        end
    elseif key == 257 or key == 335 then
    elseif key == 262 or key == 263 then
        if key == 262 then
            termUtil.cpX = termUtil.cpX + 1
        elseif key == 263 then
            termUtil.cpX = termUtil.cpX - 1
        end
    elseif key == 264 or key == 258 then
    elseif key == 265 then
    end
    termUtil.cpX = termUtil.cpX > maxXp and maxXp or termUtil.cpX
    termUtil.cpX = termUtil.cpX < minXp and minXp or termUtil.cpX
end

function absTextField:click(x, y)
    local xPos = self.x
    if x >= xPos then
        if x < xPos + #tostring(self.key[self.value]) then
            termUtil.cpX, termUtil.cpY = x, y
        else
            termUtil.cpX, termUtil.cpY = xPos + #tostring(self.key[self.value]), y
        end
    end
end

local newTextField = function(key, value, x, y)
    return setmetatable({ key = key, value = value, type = type(key[value]), x = x, y = y },
        { __index = absTextField })
end

local absSelectBox = {
    x = 1,
    y = 1,
    label = "",
    contents = {},
    count = 0,
    interval = 0,
    fontColor = "8",
    backgroundColor = "f",
    selectColor = "e"
}

function absSelectBox:paint()
    term.setCursorPos(self.x, self.y)
    local select = tostring(self.key[self.value])
    for i = 1, #self.contents, 1 do
        local str = tostring(self.contents[i])
        if select == str then
            term.blit(str, genStr(self.backgroundColor, #str), genStr(self.selectColor, #str))
        else
            term.blit(str, genStr(self.fontColor, #str), genStr(self.backgroundColor, #str))
        end
        for j = 1, self.interval, 1 do
            term.write(" ")
        end
    end
end

function absSelectBox:click(x, y)
    local xPos = x - self.x + 1
    local index = 0
    
    for i = 1, #self.contents do
        local item = tostring(self.contents[i])
        if xPos >= index and xPos <= index + #item then
            self.key[self.value] = self.contents[i]
            termUtil.cpX, termUtil.cpY = self.x, self.y
            term.setCursorPos(termUtil.cpX, termUtil.cpY)
            term.setCursorBlink(false)  
            break
        end
        index = index + #item + self.interval
    end
end

local newSelectBox = function(key, value, interval, x, y, ...)
    return setmetatable(
        { key = key, value = value, interval = interval, x = x, y = y, type = type(key[value]), contents = { ... } },
        { __index = absSelectBox })
end

local runTerm = function()
        local fieldTb = {
            spawnerName = newTextField(properties, "spawnerName", 14, 2),
            controlCenterId = newTextField(properties, "controlCenterId", 20, 3),
            password = newTextField(properties, "password", 11, 4),
            spawnerHeight = newTextField(properties, "spawnerHeight", 17, 5),
            spawnCount = newTextField(properties, "spawnCount", 14, 6),
            spawnRange = newTextField(properties, "spawnRange", 14, 7),
            despawnTime = newTextField(properties, "despawnTime", 15, 8),
            Gravity = newTextField(properties, "Gravity", 10, 9),
            Distance = newTextField(properties, "Distance", 11, 10),
            Coolingtime = newTextField(properties, "Coolingtime", 14, 13),
        }
        fieldTb.spawnerName.len = 20
        fieldTb.controlCenterId.len = 5
        fieldTb.password.len = 14
        fieldTb.spawnerHeight.len = 3
        fieldTb.spawnCount.len = 2
        fieldTb.spawnRange.len = 2
        fieldTb.Gravity.len = 4
        fieldTb.despawnTime.len = 3
        fieldTb.Distance.len = 2
        fieldTb.Coolingtime.len = 2
        local selectBoxTb = {
            mobType = newSelectBox(properties, "mobType", 2, 10, 11, "mc" , "cbc" , "cbcmw" , "warium"),
            INPUT = newSelectBox(properties, "INPUT", 2, 8, 14, "top", "left", "right", "front", "back","bottom"),
        }
        local function getEntitySelectBox()
        local mob = properties.mobType
            if mob == "mc" then
                return newSelectBox(properties, "Entity", 1, 8, 12, "fireball", "fire", "arrow","bolt","snow","dragon","trident")
            elseif mob == "cbc" then
                return newSelectBox(properties, "Entity", 2, 8, 12, "he", "smoke", "mortar", "drop", "shrapnel")
            elseif mob == "cbcmw" then
                return newSelectBox(properties, "Entity", 2, 8, 12, "heap", "hefrag","apds")
            elseif mob == "warium" then
                return newSelectBox(properties, "Entity", 1, 8, 12, "ord", "Rord", "Hord", "Sord", "clu", "kin", "tox", "fis", "fus")
            end
            return nil
        end

        local alarm_flag = false
        term.clear()
        term.setCursorPos(15, 8)
        term.write("Press any key to continue")
        while true do
            local eventData = { os.pullEvent() }
            local event = eventData[1]
            if event == "mouse_up" or event == "key_up" or event == "alarm"
                or event == "mouse_click" or event == "mouse_drag" or event == "key" or event == "char" then
                if not alarm_flag then
                    alarm_flag = true
                else
                    term.clear()
                    term.setCursorPos(1, 1)
                    printError(string.format("self id: %d", selfId))
                    term.setCursorPos(12, 1)
                    print("=== Spawner Control System ===")
                    term.setCursorPos(1, 2)
                    term.write("System Name: ")
                    term.setCursorPos(1, 3)
                    term.write("Control Center ID: ")
                    term.setCursorPos(1, 4)
                    term.write("Password: ")
                    term.setCursorPos(1, 5)
                    term.write("Spawner Height: ")
                    term.setCursorPos(1, 6)
                    term.write("Spawn Count: ")
                    term.setCursorPos(1, 7)
                    term.write("Spawn Range: ")
                    term.setCursorPos(1, 8)
                    term.write("Despawn Time: ")
                    term.setCursorPos(1, 9)
                    term.write("Gravity: ")
                    term.setCursorPos(1, 10)
                    term.write("Distance: ")
                    term.setCursorPos(1, 11)
                    term.write("Mob Type: ")
                    term.setCursorPos(1, 12)
                    term.write("Entity: ")
                    term.setCursorPos(1, 13)
                    term.write("Coolingtime: ")
                    term.setCursorPos(20, 13)
                    printError("Cooling:")
                    term.setCursorPos(2, 14)
                    term.write("INPUT: ")

                    for k, v in pairs(fieldTb) do
                        v:paint()
                    end

                    for k, v in pairs(selectBoxTb) do
                        v:paint()
                    end
                    local entitySelectBox = getEntitySelectBox()
                    if entitySelectBox then
                        entitySelectBox:paint()
                    end
                    term.setCursorPos(termUtil.cpX, termUtil.cpY)

                    if event == "mouse_click" then
                        local x, y = eventData[3], eventData[4]
                        local clickedOnField = false
                        for k, v in pairs(fieldTb) do
                            if y == v.y and x >= v.x and x <= v.x + v.len then
                                v:click(x, y)
                                clickedOnField = true
                                break
                            end
                        end
                        if not clickedOnField then
                            for k, v in pairs(selectBoxTb) do
                                if y == v.y then
                                    v:click(x, y)
                                    if k == "mobType" then
                                        entitySelectBox = getEntitySelectBox()
                                    end
                                end
                            end
                            if entitySelectBox and y == entitySelectBox.y then
                                entitySelectBox:click(x, y)
                            end
                        end

                    end
                    if event == "mouse_click" then
                        term.setCursorBlink(true)
                        local x, y = eventData[3], eventData[4]
                        local clickedOnField = false
                        for k, v in pairs(fieldTb) do
                            if y == v.y and x >= v.x and x <= v.x + v.len then
                                v:click(x, y)
                                clickedOnField = true
                                break
                            end
                        end
                        if not clickedOnField then
                            for k, v in pairs(selectBoxTb) do
                                if y == v.y then
                                    v:click(x, y)
                                    if k == "mobType" then
                                        entitySelectBox = getEntitySelectBox()
                                    end
                                end
                            end
                            if entitySelectBox and y == entitySelectBox.y then
                                entitySelectBox:click(x, y)
                            end
                        end
                    elseif event == "key" then
                        local key = eventData[2]
                        for k, v in pairs(fieldTb) do
                            if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                                v:inputKey(key)
                            end
                        end
                    elseif event == "char" then
                        local char = eventData[2]
                        for k, v in pairs(fieldTb) do
                            if termUtil.cpY == v.y and termUtil.cpX >= v.x and termUtil.cpX <= v.x + v.len then
                                v:inputChar(char)
                            end
                        end
                    end
                    system.updatePersistentData()
                end
            end
        end
end
parallel.waitForAll(runControl, listener, sendRequest, runTerm)